PraisonAI's unauthenticated A2A official example can reach real LLM-driven `eval()` tool execution
漏洞描述
## Summary The first-party PraisonAI A2A server example combines three behaviors into a remotely exploitable Critical chain: 1. The example exposes an A2A server without configuring `auth_token`. 2. The same example binds the server to `0.0.0.0`. 3. The example registers a `calculate(expression)` tool implemented with Python `eval(expression)`. An unauthenticated network client can send a JSON-RPC `message/send` request to `/a2a`. The A2A handler passes the attacker-controlled message to `agent.chat()`. With a real Gemini LLM (`gemini/gemini-2.5-flash-lite`), the model invoked the registered `calculate` tool, causing the example's `eval()` call to execute Python in the server process. The canary wrote a marker file from an unauthenticated `/a2a` request. This is not a claim that every A2A deployment is automatically RCE. The Critical chain is confirmed for the first-party A2A example, and for deployments that follow the same pattern: public unauthenticated A2A plus an unsafe tool such as this `eval()`-based `calculate` tool. The default unauthenticated A2A surface is the remote entry point; the official example's `eval()` tool provides the code execution sink. Earlier note: The unsafe official example existed earlier, but the complete unauthenticated `/a2a` `message/send` to `agent.chat()` exploit chain is only claimed here for versions where that endpoint is present and confirmed. ## Trust Boundary The boundary that should be preserved is: ```text Unauthenticated network clients must not be able to drive server-side agent tools that can execute code or mutate server state. ``` The affected example breaks that boundary. A remote unauthenticated A2A client can supply a prompt that reaches the server's LLM-backed agent. The LLM can then invoke a registered local tool. In the official example, that registered local tool directly evaluates attacker-influenced input with `eval()`. ## Vulnerable Code Official example: ```text inbox/PraisonAI/examples/python/a2a/a2a-server.py ``` Relevant lines: ```python 23 def calculate(expression: str) -> str: 24 """Calculate a mathematical expression.""" 25 try: 26 return f"Result: {eval(expression)}" 27 except Exception: 28 return "Invalid expression" 30 agent = Agent( 31 name="Research Assistant", 32 role="Research Analyst", 33 goal="Help users research topics and answer questions", 34 tools=[search_web, calculate] 35 ) 38 a2a = A2A( 39 agent=agent, 40 url="http://localhost:8000/a2a", 41 version="1.0.0" 42 ) 51 if __name__ == "__main__": 52 import uvicorn 53 uvicorn.run(app, host="0.0.0.0", port=8000) ``` A2A defaults and authentication behavior: ```text inbox/PraisonAI/src/praisonai-agents/praisonaiagents/ui/a2a/a2a.py ``` Relevant lines: ```python 125 def serve(self, host: str = "0.0.0.0", port: int = 8000): ... 142 uvicorn.run(app, host=host, port=port) 162 # Auth dependency — only applied to POST /a2a, not discovery endpoints 163 async def _verify_auth(authorization: Optional[str] = Header(None)): 164 """Verify bearer token if auth_token is configured.""" 165 if self.auth_token is None: 166 return # No auth configured — open access 192 from fastapi import Depends 193 _a2a_deps = [Depends(_verify_auth)] if self.auth_token else [] 194 @router.post("/a2a", dependencies=_a2a_deps) 195 async def handle_jsonrpc(request: Request): ``` `message/send` reaches the agent: ```python 309 try: 310 # Extract user input text 311 user_input = extract_user_input([message]) 312 313 # Run agent or agents (offload sync call to thread pool) 314 if self.agent: 315 response = await asyncio.to_thread(self.agent.chat, user_input) ``` ## Attack Model The attacker is an unauthenticated remote client that can reach the A2A HTTP service. This is realistic because the official example binds to `0.0.0.0`, does not configure `auth_token`, and exposes `/a2a`. The attacker does not need: - repository write access - local shell access - a valid bearer token - a compromised maintainer account - access to server secrets The attacker only sends a JSON-RPC request to `/a2a`. ## Non-Claims This report does not claim: - all A2A deployments are automatically RCE - `auth_token`-protected A2A deployments are affected in the same way - safe, read-only tools provide the same impact as the official example's `eval()` sink - deterministic tool invocation is required in all attacks The real LLM canary demonstrates that a normal model-backed agent can invoke the official example's unsafe tool from an unauthenticated `/a2a` request. The deterministic control proof is included only to isolate the server-to-tool sink behavior. ## Impact For the official example and similar deployments: - remote prompt-to-tool execution from an unauthenticated network request - arbitrary Python execution through the example `calculate()` tool's `eval()` - compromise of the server process privileges - potential read/write access to application files reachable by that process - potential credential or environment variable exposure if a payload reads process state - denial of service or data corruption through executed code Supporting evidence also confirmed that default unauthenticated A2A exposes task state APIs (`tasks/list`, `tasks/get`, `tasks/cancel`) and stores text plus structured `DataPart` payloads in task history. That is a separate confidentiality/integrity problem and strengthens the risk of leaving A2A unauthenticated. ## Reproduction Environment Tested repository state: ```text commit: 4985415e describe: v4.6.37-13-g4985415e ``` Real LLM used: ```text gemini/gemini-2.5-flash-lite ``` The API key value was not printed. The PoC only prints whether a provider credential is present. The PoC uses FastAPI `TestClient` to exercise the same HTTP route and request handling stack without opening a public listening socket during testing. The official example's `__main__` path binds to `0.0.0.0` when run as a server. ## Reproduction Steps From the repository root: ```bash cd <repo-root> python3 -m venv .venv-real-llm source .venv-real-llm/bin/activate python -m pip install -U pip python -m pip install litellm fastapi "pydantic>=2" httpx uvicorn ``` Set a Gemini API key without writing it to shell history: ```bash unset GEMINI_API_KEY read -rsp "GEMINI_API_KEY: " GEMINI_API_KEY echo export GEMINI_API_KEY ``` Run the real LLM canary: ```bash REAL_LLM_MODEL="gemini/gemini-2.5-flash-lite" \ REAL_LLM_TOOL_CHOICE=auto \ python out/prove-official-a2a-example-real-llm-canary.py \ | tee out/official-a2a-example-real-llm-canary-gemini-25-flash-lite-proof.log ``` Expected success marker: ```text OFFICIAL_A2A_EXAMPLE_REAL_LLM_UNAUTH_HTTP_TO_CUSTOM_EVAL_TOOL_CONFIRMED ``` Expected marker file: ```text out/official-a2a-example-real-llm-canary.txt ``` Expected marker content: ```text OFFICIAL_A2A_EXAMPLE_REAL_LLM_CONFIRMED ``` ## Real LLM Proof Log The following is the successful proof log from `out/official-a2a-example-real-llm-canary-gemini-25-flash-lite-proof.log`: ```text === Real LLM environment === model: gemini/gemini-2.5-flash-lite provider credential present: True tool_choice mode: auto API key values printed: False === Official A2A example preconditions === example path: /home/employees/fox-linux-qa-002/work/inbox/PraisonAI/examples/python/a2a/a2a-server.py A2A auth_token configured: None A2A.serve default host: '0.0.0.0' example __main__ binds host 0.0.0.0: True registered tool names: ['search_web', 'calculate'] calculate source contains eval: True calculate in DEFAULT_DANGEROUS_TOOLS: False === Real LLM HTTP-to-eval-tool result === HTTP status without Authorization: 200 marker exists: True marker path: /home/employees/fox-linux-qa-002/work/out/official-a2a-example-real-llm-canary.txt marker content matches canary: True response contains marker path: True response contains eval tool prefix: False response body p